package com.alphasystem.commerce.common.service.sequence.impl;

import static com.alphasystem.commerce.common.util.AppUtil.getResources;
import static java.lang.String.format;
import static javax.ejb.LockType.WRITE;
import static javax.ejb.TransactionManagementType.CONTAINER;
import static javax.xml.XMLConstants.W3C_XML_SCHEMA_NS_URI;

import java.net.URL;
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import javax.annotation.PostConstruct;
import javax.ejb.Lock;
import javax.ejb.Singleton;
import javax.ejb.Startup;
import javax.ejb.TransactionManagement;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import javax.xml.bind.JAXBContext;
import javax.xml.bind.JAXBElement;
import javax.xml.bind.JAXBException;
import javax.xml.bind.Unmarshaller;
import javax.xml.transform.stream.StreamSource;
import javax.xml.validation.Schema;
import javax.xml.validation.SchemaFactory;

import org.apache.log4j.Logger;
import org.xml.sax.SAXException;

import com.alphasystem.commerce.common.ApplicationException;
import com.alphasystem.commerce.common.model.AbstractEntity;
import com.alphasystem.commerce.common.model.sequence.Sequence;
import com.alphasystem.commerce.common.model.sequence.Sequences;
import com.alphasystem.commerce.common.service.sequence.SequenceService;
import com.alphasystem.commerce.common.util.AppUtil;

/**
 * @author sali
 * 
 */
@Singleton(name = "SequenceService", mappedName = "SequenceService")
@Lock(WRITE)
@Startup
@TransactionManagement(CONTAINER)
public class DefaultSequenceService implements SequenceService {

	private static final String SCHEMAS_SEQUENCE_XSD = "schemas.sequence.xsd";

	private static final String DEFAULT_SEQUENCE_NAME = "default";

	private static final String SEQUENCE_FILE = "sequence.xml";

	private static final String SEQUENCE_META_DATA_PATH = "META-INF/"
			+ SEQUENCE_FILE;

	protected static Logger logger = Logger.getLogger(SequenceService.class);

	private Map<String, Sequence> entries;

	protected EntityManager entityManager;

	@Override
	public void createSequence(Sequence entity) {
		if (logger.isDebugEnabled()) {
			logger.debug(format("Creating Sequence: %s", entity.toString()));
		}
		entity.increment();
		entries.put(entity.getName(), entity);
		entityManager.persist(entity);
	}

	@Override
	public void createSequence(String name, long initial, long blockSize,
			String prefix, String suffix) {
		createSequence(new Sequence(name, initial, blockSize, prefix, suffix));
	}

	protected String generate(String name) throws ApplicationException {
		long value = getNextLongSequenceNumber(name);
		Sequence entry = entries.get(name);
		StringBuffer seq = new StringBuffer();
		String prefix = entry.getPrefix();
		if (prefix != null) {
			seq.append(prefix);
		}
		seq.append(value);
		return seq.toString();
	}

	/**
	 * @return the entityManager
	 */
	public EntityManager getEntityManager() {
		return entityManager;
	}

	@Override
	public Long getNextLongSequenceNumber(String name)
			throws ApplicationException {
		if (logger.isDebugEnabled()) {
			logger.debug(format("Sequence Name: %s", name));
		}
		Sequence entry = getSequence(name);
		long last = entry.getLast();
		if (logger.isDebugEnabled()) {
			logger.debug(format("BEFORE: %s", entry.toString()));
		}
		if (last % entry.getBlockSize() == 0) {
			if (logger.isDebugEnabled()) {
				logger.debug(format("AFTER INCREMENTING: %s", entry.toString()));
			}
			entry.increment();
			entityManager.merge(entry);
		} else {
			entry.setLast(last + 1);
		}
		if (logger.isDebugEnabled()) {
			logger.debug(format("After: %s", entry.toString()));
		}
		return last;
	}

	@Override
	public String getNextSequenceNumber(String name)
			throws ApplicationException {
		preGenerate();
		String seq = generate(name);
		seq = postGenerate(seq, name);
		return seq;
	}

	private Sequence getSequence(String name) {
		Sequence sequence = entries.get(name);
		if (sequence == null) {
			Sequence defaultSequence = entries.get(DEFAULT_SEQUENCE_NAME);
			sequence = new Sequence(name, defaultSequence);
		}
		Sequence entity = entityManager.find(Sequence.class, name);
		if (entity == null) {
			// first time access to this sequence
			createSequence(sequence);
			entity = sequence;
		}
		if (sequence.getLast() == -1L) {
			entity.increment();
			entries.put(name, entity);
		}
		return sequence;
	}

	@SuppressWarnings("rawtypes")
	@Override
	public String getSequenceName(Class klass) {
		String sequenceName = null;
		while (true) {
			sequenceName = klass.getSimpleName();
			boolean exists = sequenceExists(sequenceName);
			if (exists) {
				break;
			}
			klass = klass.getSuperclass();
			if (klass == null
					|| klass.getName().equals(AbstractEntity.class.getName())
					|| klass.getName().equals(Object.class.getName())) {
				sequenceName = DEFAULT_SEQUENCE_NAME;
				break;
			}
		}
		logger.debug("||||||||||||||||| Class: " + klass.getName()
				+ ", Sequenec name: " + sequenceName);
		return sequenceName;
	}

	@PostConstruct
	public void init() {
		entries = Collections.synchronizedMap(new HashMap<String, Sequence>());
		Enumeration<URL> xmlData = getResources(SEQUENCE_META_DATA_PATH);
		if (xmlData == null || !xmlData.hasMoreElements()) {
			logger.warn(format("unable to find any sequence from file \"%s\".",
					SEQUENCE_FILE));
			entries.put(DEFAULT_SEQUENCE_NAME, new Sequence(
					DEFAULT_SEQUENCE_NAME, 100000L, 1000L, null, null));
		}
		while (xmlData.hasMoreElements()) {
			URL url = xmlData.nextElement();
			if (logger.isDebugEnabled()) {
				logger.debug("Found " + SEQUENCE_META_DATA_PATH + " at: "
						+ url.toString());
			}
			try {
				JAXBContext jaxbContext = JAXBContext
						.newInstance(Sequences.class.getPackage().getName());
				Unmarshaller unmarshaller = jaxbContext.createUnmarshaller();
				SchemaFactory schemaFactory = SchemaFactory
						.newInstance(W3C_XML_SCHEMA_NS_URI);
				Schema schema = schemaFactory.newSchema(new StreamSource(
						AppUtil.getResourceAsStream(SCHEMAS_SEQUENCE_XSD)));
				unmarshaller.setSchema(schema);
				@SuppressWarnings("unchecked")
				JAXBElement<Sequences> sequencesTypeElement = (JAXBElement<Sequences>) unmarshaller
						.unmarshal(url);
				Sequences sequencesType = sequencesTypeElement.getValue();
				if (sequencesType != null) {
					List<Sequence> sequenceTypes = sequencesType.getSequence();
					if (sequenceTypes != null) {
						for (Sequence entity : sequenceTypes) {
							if (!entries.containsKey(entity.getName())) {
								if (logger.isDebugEnabled()) {
									logger.debug("Adding Sequence" + entity);
								}
								entries.put(entity.getName(), entity);
							} else {
								if (logger.isDebugEnabled()) {
									logger.debug("Skipping Sequence" + entity);
								}
							}
						}
					}
				}
			} catch (JAXBException e) {
				logger.error("Unable to get JAXBContext", e);
				throw new RuntimeException(e.getMessage(), e);
			} catch (SAXException e) {
				logger.error(
						format("Unable to read schema \"5S\"",
								SCHEMAS_SEQUENCE_XSD), e);
				throw new RuntimeException(e.getMessage(), e);
			}
		}
	}

	protected String postGenerate(String value, String name) {
		Sequence entry = entries.get(name);
		String suffix = entry.getSuffix();
		if (suffix != null) {
			value = value + suffix;
		}
		return value;
	}

	protected void preGenerate() {
	}

	protected boolean sequenceExists(String name) {
		if (logger.isDebugEnabled()) {
			logger.debug("Trying to find sequence: " + name);
		}
		Sequence seq = (Sequence) entityManager.find(Sequence.class, name);
		if (logger.isDebugEnabled()) {
			if (seq == null) {
				logger.debug("No Sequence found: " + name);
			} else {
				logger.debug("Sequence found: " + seq);
			}
		}
		return seq != null;
	}

	/**
	 * @param entityManager
	 *            the entityManager to set
	 */
	@PersistenceContext
	public void setEntityManager(EntityManager entityManager) {
		this.entityManager = entityManager;
	}

	@Override
	public void updateSequence(String name, long initial, long blockSize,
			String prefix, String suffix) {
		Sequence seq = (Sequence) entityManager.find(Sequence.class, name);
		if (seq != null) {
			seq.setBlockSize(blockSize);
			seq.setInitial(initial);
			seq.setPrefix(prefix);
			seq.setSuffix(suffix);
			entityManager.merge(seq);
			entries.put(name, seq);
		}
	}

}
